﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net.Sockets;
using System.Net;
using System.IO;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Security.Authentication;
using IP2Client;

namespace IP2Server
{
    class Program
    {
        private static BinaryFormatter formatter;
        private static bool readClientStream;
        private static X509Certificate serverCertificate;

        private bool acceptConnections;
        private List<Thread> threads;

        /// <summary>
        /// Entrypoint
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            new Program();
        }

        /// <summary>
        /// Constructor for the Program class
        /// </summary>
        public Program()
        {
            formatter = new BinaryFormatter();
            readClientStream = true;
            acceptConnections = true;
            threads = new List<Thread>();

            UserStorage.OpenAllUsers();

            bool containsDoctor = false;

            foreach (User u in UserStorage.allUsers)
            {
                if (u.AccessRights == "DOCTOR")
                {
                    containsDoctor = true;
                    continue;
                }
            }

            if (!containsDoctor)
                UserStorage.AddUser(new User("Admin", ServerSecurity.CalculateMD5Hash("Admin"), "DOCTOR"));

            startProgram();
        }

        /// <summary>
        /// Creates a server certificate and sets it to the predefined field
        /// </summary>
        /// <author>Tim Cocu</author>
        private void createAndSetCertificate()
        {
            byte[] c = Certificate.CreateSelfSignCertificatePfx("CN=127.0.0.1", DateTime.Parse("2000-01-01"), DateTime.Parse("2020-01-01"), "a2");
            serverCertificate = new X509Certificate2(c, "a2");
        }

        /// <summary>
        /// Method for starting up the program.
        /// Start the TcpListener, check login and make new threads for each connected user
        /// </summary>
        private void startProgram()
        {
            createAndSetCertificate();

            TcpListener listener = new TcpListener(IPAddress.Any, 1338);
            listener.Start();

            while (acceptConnections)
            {
                Console.WriteLine("Waiting for connection...");

                //AcceptTcpClient waits for a connection from the client
                TcpClient client = listener.AcceptTcpClient();
                IPAddress incomingIp = ((IPEndPoint)client.Client.RemoteEndPoint).Address;

                //Immidiately setup the SSL-stream. This secures login!
                SslStream sslStream = new SslStream(client.GetStream(), false);
                sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls, true);

                //Read the login details
                string strReceivedLoginDetails = readObject(sslStream) as string;
                string[] loginDetails;

                if (strReceivedLoginDetails != null)
                    loginDetails = strReceivedLoginDetails.Split(null);
                else
                {
                    Console.WriteLine("Error handling login..");
                    return;
                }

                if (loginDetails.Length == 2)
                {
                    Console.WriteLine("Found: " + loginDetails[0] + " " + loginDetails[1]);

                    //Validate login
                    User returnedUser = UserStorage.ValidateUserLogin(loginDetails);

                    //If user logged in successfully
                    if (returnedUser != null)
                    {
                        returnedUser.IPAddress = incomingIp;
                        returnedUser.TcpClient = client;
                        returnedUser.sslStream = sslStream;

                        string response = returnedUser.AccessRights;
                        Console.WriteLine(response);
                        //Send a response to the connected client
                        sendObject(client, sslStream, response);

                        //Create a new thread, start it and notify the user that the thread has been created
                        Thread thread = new Thread(new ParameterizedThreadStart(HandleClientThread));
                        thread.IsBackground = true;
                        threads.Add(thread);
                        thread.Start((object)returnedUser);
                        returnedUser.NotifyThreadCreated();

                        Console.WriteLine("Started thread for:");
                        Console.WriteLine("{0}\n", returnedUser.UserName);
                    }
                    else
                    {
                        //Login failed, send message REJECTED
                        sendObject(client, sslStream, "REJECTED");
                        Console.WriteLine("Rejected: {0} {1}", loginDetails[0], loginDetails[1]);
                        Console.WriteLine("From: {0}\n", incomingIp);
                    }
                }
            }
        }

        /// <summary>
        /// Method for retrieving a List of all connected users
        /// </summary>
        /// <returns>A List of all connected users</returns>
        private List<string> getAllConnectedUsers()
        {
            List<string> userNames = new List<string>();

            Console.WriteLine("Connected users: ");

            foreach (User u in UserStorage.allUsers)
            {
                if (u.TcpClient.Connected)
                {
                    userNames.Add(u.UserName);
                    Console.WriteLine(u.UserName);
                }
            }

            return userNames;
        }

        /// <summary>
        /// Thread for handling a single connected client
        /// </summary>
        /// <param name="obj">The User to start the thread for</param>
        private void HandleClientThread(object obj)
        {
            User user = obj as User;
            TcpClient client = user.TcpClient;

            while (readClientStream)
            {
                object receivedObject = null;

                //If data is available
                if (client.Connected && client.Available > 0)
                {
                    //Read the object that is available
                    receivedObject = readObject(user.sslStream);
                    Console.WriteLine(receivedObject.GetType());

                    if(receivedObject is string)
                    {
                        string sRcvdText = receivedObject as string;

                        if (sRcvdText != null)
                        {
                            //Write what was received
                            Console.WriteLine("Received message: {0}", sRcvdText);

                            //Call handleCommand to handle the command
                            handleCommand(user, sRcvdText);
                        }
                    }
                    else if (receivedObject is List<Measurement>)
                    {
                        Console.WriteLine(user.getFullSessionPath());
                        saveMeasurement(user.getFullSessionPath(), receivedObject as List<Measurement>);
                    } 
                }

                //Send the latest Measurement of the selected user (in doctorclient) to the doctor
                if (user.AccessRights == "DOCTOR" && user.SelectedUser != string.Empty)
                {
                    foreach (User u in UserStorage.allUsers)
                    {
                        if (u.UserName.Equals(user.SelectedUser) && u.Measurements.Count > 0)
                        {
                            sendObject(user.TcpClient, user.sslStream, u.Measurements.Last());
                            continue;
                        }
                    }
                }
            }

            //Close the Stream and TcpClient
            client.GetStream().Dispose();
            client.Close();
            Console.WriteLine("Connection closed.");
        }

        /// <summary>
        /// Method for handling a command sent by a User
        /// </summary>
        /// <param name="sendingUser">User that sent the command</param>
        /// <param name="sRcvdText">The command that was sent, split on spaces</param>
        private void handleCommand(User sendingUser, string sRcvdText)
        {
            TcpClient client = sendingUser.TcpClient;
            string stringToSend = string.Empty;
            User extractedUser = null;
            string[] sRcvdTextSplit = sRcvdText.Split(' ');

            if(sRcvdTextSplit.Length > 1)
                extractedUser = GetUser(sRcvdTextSplit[1]);

            //Switch on the command given
            switch (sRcvdTextSplit[0])
            {
                case "!sndAllPatients":
                    List<string> names = new List<string>();

                    foreach (User u in UserStorage.allUsers)
                    {
                        if (u.AccessRights == "PATIENT")
                        {
                            names.Add(u.UserName);
                            Console.WriteLine(u.UserName);
                        }
                    }

                    sendObject(client, sendingUser.sslStream, names);
                    break;
                case "!addUser":
                    if (sendingUser.AccessRights == "DOCTOR")
                    {
                        if (sRcvdTextSplit[3].ToUpper() == "DOCTOR")
                        {
                            string hashedPw = ServerSecurity.CalculateMD5Hash(sRcvdTextSplit[2]);
                            UserStorage.AddUser(new User(sRcvdTextSplit[1], hashedPw, "DOCTOR", null));
                        }
                        else if (sRcvdTextSplit[3].ToUpper() == "PATIENT")
                        {
                            string hashedPw = ServerSecurity.CalculateMD5Hash(sRcvdTextSplit[2]);
                            UserStorage.AddUser(new User(sRcvdTextSplit[1], hashedPw, "PATIENT", sRcvdTextSplit[4]));
                        }
                    }
                    break;
                case "!broadcast":
                    stringToSend += sendingUser.UserName + ":";

                    for (int i = 1; i < sRcvdTextSplit.Length; i++)
                    {
                        stringToSend += " " + sRcvdTextSplit[i];
                    }

                    foreach (User targetUser in UserStorage.allUsers)
                    {
                        TcpClient targetUserClient = targetUser.TcpClient;
                        if (targetUserClient != null)
                        {
                            if (targetUserClient.Connected && targetUser.UserName != sendingUser.UserName)
                            {
                                sendObject(targetUserClient, targetUser.sslStream, stringToSend);
                            }
                        }
                    }
                    break;
                case "!sndSessionList":
                    List<String> fileNames = new List<String>();
                    try
                    {
                        string[] subDirs = Directory.GetDirectories(extractedUser.UserFolder);

                        foreach (string subDir in subDirs)
                        {
                            string[] filesFound = Directory.GetFiles(subDir);


                            string dirName = new DirectoryInfo(subDir).Name;

                            foreach (string file in filesFound)
                            {
                                string fileName = dirName + " - " + new FileInfo(file).Name;
                                fileNames.Add(fileName);
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                    }
                    sendObject(client, sendingUser.sslStream, fileNames);
                    break;
                case "!sndSessionFile":
                    List<Measurement> measurements = new List<Measurement>();
                    try
                    {
                        string path = extractedUser.UserFolder + sRcvdTextSplit[2] + @"\" + sRcvdTextSplit[3];
                        Console.WriteLine("Opening and sending {0}", path);
                        measurements = openMeasurements(path);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e.Message);
                    }
                    sendObject(client, sendingUser.sslStream, measurements);
                    break;
                case "!disconnect":
                    Console.WriteLine("{0} disconnected from the server", sendingUser.UserName);
                    sendingUser.TcpClient.GetStream().Close();
                    sendingUser.TcpClient.Close();
                    foreach (Thread t in threads)
                    {
                        if (t == Thread.CurrentThread)
                        {
                            //Forcefully abort the thread by throwing a ThreadAbortedException,
                            //not the best way but it works
                            t.Abort();
                        }
                    }
                    break;
            }
        }

        /// <summary>
        /// Method for retrieving a User from a userName
        /// </summary>
        /// <param name="userName">The name of the user</param>
        /// <returns>A User of found, else null</returns>
        public User GetUser(string userName)
        {
            foreach (User u in UserStorage.allUsers)
            {
                if (u.UserName == userName)
                {
                    return u;
                }
            }
            return null;
        }

        /// <summary>
        /// Method for sending a message to the doctor of the patient
        /// </summary>
        /// <param name="user">The user that issued the command</param>
        /// <param name="sRcvdTextSplit">The text to send to the doctor</param>
        private void sendMessageToDoctor(User user, string stringToSend)
        {
            foreach (User u in UserStorage.allUsers)
            {
                if (u.UserName.Equals(user.DoctorName))
                {
                    if (u.TcpClient.Connected)
                    {
                        sendObject(u.TcpClient, u.sslStream, stringToSend);

                        continue;
                    }
                    else
                    {
                        Console.WriteLine("Doctor is not connected..");
                    }
                }
            }
        }

        /// <summary>
        /// Method for saving a measurement to a file
        /// </summary>
        /// <param name="path">The path to the file to save to</param>
        /// <param name="rcvdMeasurement">The measurement to save</param>
        private static void saveMeasurement(string path, List<Measurement> rcvdMeasurement)
        {
            try
            {
                //Open a FileStream allowed to write and append to a file
                FileStream saveStream = new FileStream(path, FileMode.Append, FileAccess.Write);

                using (saveStream)
                {
                    formatter.Serialize(saveStream, rcvdMeasurement);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        /// <summary>
        /// Method for opening a measurement file
        /// </summary>
        /// <param name="path">The path to the file</param>
        /// <returns>A List of Measurements</returns>
        private static List<Measurement> openMeasurements(string path)
        {
            List<Measurement> tmpMeasurements = new List<Measurement>();

            try
            {
                //Open a FileStream alowed to open and read files
                FileStream readStream = new FileStream(path, FileMode.Open, FileAccess.Read);
                readStream.Position = 0;

                using (readStream)
                {
                    //Deserializen en dus ophalen die handel!
                    IFormatter readFormatter = new BinaryFormatter();

                    tmpMeasurements = (List<Measurement>)readFormatter.Deserialize(readStream);
                }
            }
            catch (SerializationException)
            {
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }

            return tmpMeasurements;
        }

        /// <summary>
        /// Method for reading objects from a TcpClient's Stream
        /// </summary>
        /// <param name="client"></param>
        /// <returns>An object read from the TcpClient's Stream</returns>
        private object readObject(SslStream clientSSLStream)
        {
            object objTmp = null;

            try
            {
                objTmp = (object)formatter.Deserialize(clientSSLStream);
            }
            catch (Exception)
            {
                readClientStream = false;
            }

            return objTmp;
        }

        /// <summary>
        /// Method for sending an object to a client
        /// </summary>
        /// <param name="client">The client to send the object to</param>
        /// <param name="objToSend">The object to send</param>
        private void sendObject(TcpClient client, SslStream clientSSLStream, object objToSend)
        {

            //Is deze mofo wel connected?
            //Extra foutafhandeling.
            if (client.Connected)
            {
                //Kijken of er geen lege stream word geretourneerd.
                if (objToSend is List<Measurement> || objToSend is string || objToSend is List<string>)
                {
                    try
                    {
                        formatter.Serialize(clientSSLStream, objToSend);
                        Console.WriteLine("Done sending");
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Error sending message: {0}. Closing TcpClient", e.Message);
                        clientSSLStream.Close();
                        client.Close();
                    }
                }
                else
                    Console.WriteLine(objToSend.ToString());
            }
        }
    }
}
